W3. Разделяй и властвуй (divide and conquer), master theorem, эффективная сортировка

Автор

Nikolai Kudasov

Дата публикации

2 февраля 2026 г.

1. Краткое содержание

1.1 Введение в divide and conquer

Divide and conquer — базовый приём проектирования алгоритмов: задача разбивается на меньшие независимые подзадачи того же типа, каждая решается отдельно, затем ответы combine в ответ исходной задачи. Особенно силён там, где декомпозиция естественна.

Стратегия по сути рекурсивна: каждый вызов решает меньший экземпляр до base case (подзадача уже «маленькая»), после чего при раскрутке стека результаты объединяются.

1.1.1 Три части divide and conquer

У любого такого алгоритма три компонента:

  1. Divide: разбить задачу на подзадачи того же типа, обычно уменьшив размер входа (например, разрезать массив пополам или снизить степень в exponentiation).
  2. Conquer: решить каждую подзадачу рекурсивно. Base case задаёт порог, когда рекурсия не нужна (массив из одного элемента уже «отсортирован»).
  3. Combine: слить решения подзадач в решение исходной задачи. Этот шаг должен быть дёшев: иначе он доминирует во времени.

Наглядно: большая библиотека — не сортировать всё сразу, а разбить на отсеки, отсортировать каждый (conquer), затем слить отсортированные части (combine). Слияние двух уже отсортированных списков существенно проще, чем сортировать один несортированный.

1.2 Анализ divide and conquer: рекуррентности

Время естественно записывается рекуррентностью (recurrence relation) — уравнением на \(T(n)\) через время на меньших размерах.

Типичный вид: \[T(n) = \begin{cases} \Theta(1) & \text{if } n \le n_0 \text{ (base case)} \\ a \cdot T(n/b) + f(n) & \text{otherwise (recursive case)} \end{cases}\]

  • \(a\): число подзадач на шаге divide
  • \(n/b\): размер каждой подзадачи (вход делится на \(b\))
  • \(f(n)\): время на divide и combine без учёта рекурсивных вызовов

Пример: у merge sort \(a=2\), размеры \(n/b=n/2\), слияние \(f(n)=\Theta(n)\): \[T(n) = 2 \cdot T(n/2) + \Theta(n)\]

Умение решать рекуррентности даёт асимптотику всего алгоритма.

1.2.1 Способы решения рекуррентностей

Три основных подхода:

  1. Substitution method: угадать границу и доказать индукцией; нужна интуиция.
  2. Recursion tree method: дерево рекурсии, сумма работы по уровням, геометрические ряды; показывает, где «тратится» время.
  3. Master method: рецепт для \(T(n)=aT(n/b)+f(n)\); на практике чаще всего достаточно выписать \(a\), \(b\), \(f(n)\).
1.3 Master theorem

Master theorem (Cormen et al., Theorem 4.1) даёт готовый ответ для \[T(n) = a \cdot T(n/b) + f(n)\] при \(a\ge 1\), \(b>1\) и асимптотически неотрицательном \(f(n)\). Сравнивают рост \(f(n)\) (стоимость divide/combine) с \(n^{\log_b a}\) (совокупная «масса» рекурсивных вызовов).

1.3.1 Три случая

Case 1 — доминирует рекурсия: если \(f(n)\) существенно меньше \(n^{\log_b a}\), время задаёт рекурсия.

\[\text{If } \exists\, \epsilon > 0 \text{ such that } f(n) = O(n^{\log_b a - \epsilon}), \text{ then } T(n) = \Theta(n^{\log_b a})\]

Смысл: основная работа на листьях дерева рекурсии; листьев \(\Theta(n^{\log_b a})\), на листе \(\Theta(1)\).

Case 2 — баланс: если \(f(n)\) того же порядка, что \(n^{\log_b a}\) (с возможными логарифмическими множителями), вклад «по уровням» сопоставим.

\[\text{If } \exists\, k \ge 0 \text{ such that } f(n) = \Theta(n^{\log_b a} \log^k n), \text{ then } T(n) = \Theta(n^{\log_b a} \log^{k+1} n)\]

Смысл: работа распределена по уровням; число уровней \(\Theta(\log_b n)\); логарифмические факторы в \(f(n)\) накапливаются.

Case 3 — доминирует combine: если \(f(n)\) существенно больше \(n^{\log_b a}\), главный вклад даёт divide/combine.

\[\text{If } \exists\, \epsilon > 0 \text{ such that } f(n) = \Omega(n^{\log_b a + \epsilon}), \text{ and the **regularity condition** } af(n/b) \le c \cdot f(n)\] \[\text{holds for some } c < 1 \text{ and all sufficiently large } n, \text{ then } T(n) = \Theta(f(n))\]

Смысл: верхний уровень рекурсии тянет основное время; regularity condition не даёт стоимости combine «взрываться» при подъёме по дереву.

Что такое regularity condition? После divide и combine на уровне \(i\) работа не должна сильно превосходить уровень \(i-1\) — тогда ряд стоимостей ведёт себя как сходящийся геометрический ряд, и суммарно доминирует верх.

1.3.2 Смысл \(n^{\log_b a}\)

Это «чистая» стоимость рекурсии — суммарная работа всех рекурсивных вызовов без учёта divide/combine:

  • на уровне 1 — \(a\) подзадач размера \(n/b\), вклад \(a\cdot T(n/b)\);
  • на уровне 0 (корень) — одна задача размера \(n\);
  • на уровне \(k\)\(a^k\) задач размера \(n/b^k\);
  • глубина \(\log_b n\);
  • число листьев: \(a^{\log_b n}=n^{\log_b a}\).
1.4 Задача о максимальном подмассиве (Maximum Subarray Problem)

Maximum Subarray Problem — классический пример задачи, к которой естественно применим divide and conquer, и наглядно показывающий, как этот приём улучшает наивные алгоритмы.

1.4.1 Постановка задачи

Вход: последовательность из \(n\) чисел \(\langle a_1, a_2, \dots, a_n \rangle\).

Выход: индексы \(l\) и \(r\) такие, что \(\sum_{i=l}^{r} a_i \ge \sum_{i=l'}^{r'} a_i\) для любых \(1 \le l' \le r' \le n\). Нужно найти непрерывный подмассив с максимальной суммой.

Практика: задача часто встречается в финансовом анализе, когда отслеживают изменения цены акции (прибыль/убыток): максимальный подмассив соответствует «лучшему окну» для покупки и продажи. Аналогично её используют при анализе температурных рядов или сейсмических данных — чтобы найти интервалы, где условия устойчиво «хорошие» или «плохие».

Пример: для массива \([2, -1, 3, 4, 1, -5]\) подмассив \([-1, 3, 4, 1]\) (с индекса 2 по 5) имеет сумму 7, и это максимум.

1.4.2 Наивный перебор (brute force)

Прямолинейный алгоритм проверяет каждый возможный подмассив:

for l = 1 to n
  for r = l to n
    вычислить сумму A[l...r] в sum
    если sum > best_sum:
      best_sum = sum
      best_l = l
      best_r = r

Внутренние циклы перебирают все \(\binom{n+1}{2} = \Theta(n^2)\) подмассивов. Даже при оптимизации (накапливая суммы по шагам) в лучшем случае остаётся \(\Theta(n^2)\), что для больших \(n\) медленно.

1.4.3 Решение divide and conquer

Ключевая идея: максимальный подмассив в \(A[l \dots r]\) лежит ровно в одном из трёх вариантов:

  1. Целиком в левой половине\(A[l \dots m]\), где \(m = \lfloor (l + r) / 2 \rfloor\)
  2. Целиком в правой половине\(A[m + 1 \dots r]\)
  3. Пересекает середину — тянется от какого-то индекса слева до какого-то справа

В случаях 1 и 2 рекурсируем. В случае 3 нужен отдельный приём: рекурсией нельзя «собрать» подмассивы, которые пересекают точку деления.

Пересекающий подмассив находится за линейное время: от середины расширяемся влево и вправо, на каждом шаге отслеживая лучшую сумму. Это \(O(n)\) без дополнительной рекурсии.

Структура алгоритма:

FIND-MAXIMUM-SUBARRAY(A, l, r):
  if l == r:
    return (l, r, A[l])  // база: один элемент
  else:
    m = floor((l + r) / 2)
    (left_l, left_r, left_sum) = FIND-MAXIMUM-SUBARRAY(A, l, m)
    (right_l, right_r, right_sum) = FIND-MAXIMUM-SUBARRAY(A, m + 1, r)
    (cross_l, cross_r, cross_sum) = FIND-MAX-CROSSING-SUBARRAY(A, l, m, r)

    // максимум из трёх вариантов
    if left_sum >= right_sum and left_sum >= cross_sum:
      return (left_l, left_r, left_sum)
    else if right_sum >= left_sum and right_sum >= cross_sum:
      return (right_l, right_r, right_sum)
    else:
      return (cross_l, cross_r, cross_sum)

Вспомогательная процедура FIND-MAX-CROSSING-SUBARRAY находит лучший пересекающий подмассив:

FIND-MAX-CROSSING-SUBARRAY(A, l, m, r):
  // лучшая сумма, начиная с m и расширяясь влево
  left_sum = -∞
  sum = 0
  for i = m down to l:
    sum = sum + A[i]
    if sum > left_sum:
      left_sum = sum
      max_left = i

  // лучшая сумма, начиная с m+1 и расширяясь вправо
  right_sum = -∞
  sum = 0
  for j = m + 1 to r:
    sum = sum + A[j]
    if sum > right_sum:
      right_sum = sum
      max_right = j

  return (max_left, max_right, left_sum + right_sum)

Эта процедура работает за \(\Theta(n)\): один проход от середины к краям.

1.4.4 Анализ сложности

Рекуррентность: \[T(n) = 2 \cdot T(n/2) + \Theta(n)\]

  • Divide: \(\Theta(1)\) — найти середину
  • Conquer: два рекурсивных вызова, каждый на \(n/2\) элементах
  • Combine: \(\Theta(n)\) — найти пересекающий подмассив

Применяем master theorem: \(a = 2\), \(b = 2\), \(f(n) = \Theta(n)\).

Считаем \(n^{\log_b a} = n^{\log_2 2} = n^1 = n\).

Так как \(f(n) = \Theta(n^1) = \Theta(n)\), это Case 2\(k = 0\)): \[T(n) = \Theta(n \log n)\]

Это существенное улучшение по сравнению с наивным перебором \(\Theta(n^2)\), особенно при больших \(n\).

1.5 Сортировка слиянием (merge sort)

Merge sort — один из ключевых алгоритмов сортировки. Это «чистый» divide and conquer, который показывает, как парадигма даёт эффективные и практичные решения.

1.5.1 Обзор и свойства
  • Тип: divide and conquer
  • Время: \(\Theta(n \log n)\) — гарантированно в лучшем и худшем случаях
  • Память: \(\Theta(n)\) — нужна дополнительная память для слияния
  • Устойчивость (stability): стабильна — равные элементы сохраняют относительный порядок
  • In-place: не in-place — при слиянии нужны временные массивы

Устойчивость важна при сортировке объектов с несколькими полями: например, если записи сотрудников сначала отсортированы по имени, а затем по отделу, стабильная сортировка сохранит внутри одного отдела прежний порядок по имени.

1.5.2 Идея алгоритма
  1. Divide: разрезать массив пополам в середине.
  2. Conquer: рекурсивно отсортировать левую и правую половины.
  3. Combine: слить две отсортированные половины в один отсортированный массив.

Смысл: слияние двух уже отсортированных массивов — простая операция, линейная по суммарному размеру; это сильно быстрее, чем сортировать один несортированный массив «с нуля».

1.5.3 Операция merge (слияние)

Слияние двух отсортированных массивов \(L\) и \(R\) в отсортированный \(A\):

  1. Держим указатели i и j в начале \(L\) и \(R\).
  2. Сравниваем L[i] и R[j], копируем меньший элемент в \(A\) и сдвигаем соответствующий указатель.
  3. Когда один массив исчерпан, дописываем хвост из другого.

Пример: слияние \(L = [1, 4, 5, 6, 8]\) и \(R = [2, 3, 4, 7, 9]\):

  • 1 и 2: копируем 1 → \(A = [1]\)
  • 4 и 2: копируем 2 → \(A = [1, 2]\)
  • 4 и 3: копируем 3 → \(A = [1, 2, 3]\)
  • 4 и 4: копируем 4 из \(L\) (для stability) → \(A = [1, 2, 3, 4]\)
  • далее → \(A = [1, 2, 3, 4, 4, 5, 6, 7, 8, 9]\)

Для устойчивости: при равных L[i] и R[j] всегда берём элемент из левого массива \(L\) — так сохраняется исходный относительный порядок.

Время: \(\Theta(n)\) при \(n = |L| + |R|\), каждый элемент просматривается ровно один раз.

1.5.4 Псевдокод
MERGE-SORT(A, p, r):
  if p >= r:
    return  // 0 или 1 элемент уже «отсортирован»
  q = floor((p + r) / 2)
  MERGE-SORT(A, p, q)        // сортировать левую половину
  MERGE-SORT(A, q + 1, r)    // сортировать правую половину
  MERGE(A, p, q, r)          // слить половины
1.5.5 Анализ сложности

Рекуррентность: \[T(n) = 2 \cdot T(n/2) + \Theta(n)\]

  • Divide: \(\Theta(1)\) — вычислить середину
  • Conquer: два рекурсивных вызова на половинах размера \(n/2\)
  • Combine: \(\Theta(n)\) — слияние за линейное время

Master theorem: \(a = 2\), \(b = 2\), \(f(n) = \Theta(n)\).

Так как \(n^{\log_2 2} = n\) и \(f(n) = \Theta(n)\), это Case 2\(k = 0\)): \[T(n) = \Theta(n \log n)\]

Память: нужно \(\Theta(n)\) дополнительной памяти под временные буферы при слиянии — главный минус по сравнению с in-place сортировками.

1.6 Быстрая сортировка (quicksort)

Quicksort — один из самых распространённых на практике алгоритмов сортировки, хотя худший случай у него неприятен. При удачном выборе pivot среднее время отличное, а дополнительная память невелика.

1.6.1 Обзор и свойства
  • Тип: divide and conquer
  • Время: худший случай \(\Theta(n^2)\), среднее \(\Theta(n \log n)\)
  • Память: \(\Theta(\log n)\) под стек рекурсии
  • Устойчивость: не стабильна — при разбиении равные элементы могут «перепрыгивать»
  • In-place: да — разбиение переставляет элементы на месте, с минимумом доп. памяти

Популярность quicksort — из сильного среднего случая и скромных требований к памяти. В реализациях часто берут случайный pivot, чтобы ожидаемое время оставалось \(\Theta(n \log n)\) даже на «враждебных» входах.

1.6.2 Идея алгоритма
  1. Divide: выбрать pivot и разбить массив на три группы:
    • элементы \(\le\) pivot
    • сам pivot
    • элементы \(>\) pivot
  2. Conquer: рекурсивно отсортировать левую часть (\(\le\) pivot) и правую (\(>\) pivot).
  3. Combine: по сути конкатенация — pivot уже на финальной позиции, обе стороны отсортированы, значит весь массив отсортирован.

Смысл: в отличие от merge sort, шаг combine здесь неявный: после рекурсии дополнительной «склейки» не нужно.

1.6.3 Алгоритм разбиения (partition)

Partition — сердце quicksort: переставляет массив так, что все элементы \(\le\) выбранного pivot оказываются слева, а все \(>\) — справа.

In-place partition:

PARTITION(A, p, r):
  x = A[r]           // pivot — последний элемент
  i = p - 1          // граница низкой части

  for j = p to r - 1:     // все, кроме pivot
    if A[j] <= x:
      i = i + 1
      exchange A[i] with A[j]  // в низкую часть

  exchange A[i + 1] with A[r]  // pivot на финальную позицию
  return i + 1        // индекс pivot

Инварианты на каждом шаге:

  • \(A[p \dots i]\): элементы \(\le x\) (низкая часть)
  • \(A[i+1 \dots j-1]\): элементы \(> x\) (высокая часть)
  • \(A[j \dots r-1]\): ещё не обработаны
  • \(A[r]\): pivot

Время: \(\Theta(n)\) — один проход по элементам.

1.6.4 Псевдокод
QUICKSORT(A, p, r):
  if p < r:
    q = PARTITION(A, p, r)     // partition, индекс pivot
    QUICKSORT(A, p, q - 1)     // сортировать ≤ pivot
    QUICKSORT(A, q + 1, r)     // сортировать > pivot
1.6.5 Худший случай

Худший случай — когда pivot всегда минимальный или максимальный: разбиение сильно перекошено (в одной части \(n-1\) элементов, в другой 0).

Рекуррентность: \[T(n) = T(n-1) + T(0) + \Theta(n) = T(n-1) + \Theta(n)\]

Разворачивая: \[T(n) \le T(n-1) + cn \le T(n-2) + c(n-1) + cn \le \dots \le T(0) + c \cdot 2 + c \cdot 3 + \dots + cn\] \[= c \sum_{i=2}^{n} i = c \cdot \frac{n(n+1)}{2} - c = \Theta(n^2)\]

Типичный сценарий: вход уже отсортирован (или в обратном порядке), а pivot всегда первый или последний элемент.

1.6.6 Лучший случай

Лучший случай — когда pivot делит массив почти пополам.

Рекуррентность: \[T(n) = 2 \cdot T(n/2) + \Theta(n)\]

Master theorem: \(a = 2\), \(b = 2\), \(f(n) = \Theta(n)\).

Так как \(f(n) = \Theta(n^{\log_2 2})\), это Case 2\(k = 0\)): \[T(n) = \Theta(n \log n)\]

1.6.7 Средний случай

Со случайным pivot (или эвристикой вроде «median of three») разбиения обычно «разумные». Формальный анализ показывает: даже при несбалансированных частях ожидаемое время остаётся \(\Theta(n \log n)\).

Наглядно: даже если в одной части четверть элементов, а в другой три четверти, суммарные размеры по уровням рекурсии всё равно убывают достаточно быстро, и глубина логарифмическая.

Формально (Cormen et al., 2022, разд. 7.4) для рандомизированного quicksort: \[E[T(n)] = \Theta(n \log n)\]

1.7 Сравнение алгоритмов сортировки

У разных сортировок разные компромиссы:

Алгоритм Лучший Средний Худший Память Stable In-place
Insertion sort \(\Theta(n)\) \(\Theta(n^2)\) \(\Theta(n^2)\) \(\Theta(1)\) да да
Merge sort \(\Theta(n \log n)\) \(\Theta(n \log n)\) \(\Theta(n \log n)\) \(\Theta(n)\) да нет
Quicksort \(\Theta(n \log n)\) \(\Theta(n \log n)\) \(\Theta(n^2)\) \(\Theta(\log n)\) нет да
Heapsort \(\Theta(n \log n)\) \(\Theta(n \log n)\) \(\Theta(n \log n)\) \(\Theta(1)\) нет да

Как выбирать:

  • Merge sort: когда нужен худший случай \(\Theta(n \log n)\) или критична stability; платите \(\Theta(n)\) памяти.
  • Quicksort: «универсальная» сортировка: сильное среднее и мало памяти; на уже почти отсортированных данных без рандомизации pivot осторожнее.
  • Insertion sort: для очень маленьких \(n\) (часто \(n < 20\)) или почти отсортированных массивов — малый накладной расход.
  • Гибрид: в реальных библиотеках часто quicksort + переход на insertion sort на маленьких подмассивах (типичный порог \(n \le 10\)).

2. Определения

  • Алгоритм divide and conquer (divide-and-conquer algorithm): рекурсивный алгоритм, который решает задачу, разбивая её на меньшие независимые подзадачи того же типа, рекурсивно решая каждую (conquer) и объединяя ответы (combine).
  • Divide: шаг, на котором задача делится на подзадачи, обычно уменьшая размер входа.
  • Conquer: шаг, на котором подзадачи рекурсивно решаются до base case (подзадача достаточно мала, чтобы решить её напрямую).
  • Combine: шаг, на котором решения подзадач сливаются в решение исходной задачи.
  • Рекуррентность (recurrence relation): уравнение, выражающее время \(T(n)\) рекурсивного алгоритма через время на меньших размерах; основной инструмент анализа divide and conquer.
  • Master theorem: прямой способ решать рекуррентности вида \(T(n) = a \cdot T(n/b) + f(n)\), сравнивая \(f(n)\) с \(n^{\log_b a}\).
  • Case 1 (master theorem): если \(f(n) = O(n^{\log_b a - \epsilon})\) при некотором \(\epsilon > 0\), доминирует рекурсия и \(T(n) = \Theta(n^{\log_b a})\).
  • Case 2 (master theorem): если \(f(n) = \Theta(n^{\log_b a} \log^k n)\) при некотором \(k \ge 0\), divide и рекурсия сбалансированы и \(T(n) = \Theta(n^{\log_b a} \log^{k+1} n)\).
  • Case 3 (master theorem): если \(f(n) = \Omega(n^{\log_b a + \epsilon})\) при некотором \(\epsilon > 0\) и выполнена regularity condition, доминирует divide/combine и \(T(n) = \Theta(f(n))\).
  • Regularity condition: в Case 3 условие \(af(n/b) \le c \cdot f(n)\) при некотором \(c < 1\); гарантирует «хорошее» поведение ряда стоимостей по уровням рекурсии.
  • Maximum Subarray Problem: найти непрерывный подмассив максимальной суммы; часто в финансах и временных рядах.
  • Brute force: алгоритм, систематически перебирающий все варианты; часто неэффективен, но прост для реализации и понимания.
  • Merge sort: стабильная сортировка divide and conquer с \(\Theta(n \log n)\) во всех случаях и дополнительной памятью \(\Theta(n)\).
  • Merge (слияние): операция объединения двух отсортированных массивов в один отсортированный за линейное время.
  • Stability (устойчивость сортировки): равные элементы сохраняют исходный относительный порядок после сортировки.
  • Quicksort: in-place сортировка divide and conquer со средним \(\Theta(n \log n)\) и худшим \(\Theta(n^2)\); на случайных входах часто очень быстра на практике.
  • Pivot: элемент, вокруг которого в quicksort переставляют массив на «меньше/больше».
  • Partition: операция quicksort, переставляющая массив так, что элементы \(\le\) pivot идут раньше элементов \(>\) pivot.
  • In-place sorting: сортировка без доп. памяти, пропорциональной размеру входа; обычно \(O(1)\) или \(O(\log n)\) сверху.

3. Формулы

  • Вид рекуррентности для master theorem: \(T(n) = a \cdot T(n/b) + f(n)\) при \(a \ge 1\), \(b > 1\) и асимптотически неотрицательном \(f(n)\)
  • Case 1: если \(f(n) = O(n^{\log_b a - \epsilon})\) при некотором \(\epsilon > 0\), то \(T(n) = \Theta(n^{\log_b a})\)
  • Case 2: если \(f(n) = \Theta(n^{\log_b a} \log^k n)\) при некотором \(k \ge 0\), то \(T(n) = \Theta(n^{\log_b a} \log^{k+1} n)\)
  • Case 3: если \(f(n) = \Omega(n^{\log_b a + \epsilon})\) при некотором \(\epsilon > 0\) и \(af(n/b) \le cf(n)\) при \(c < 1\), то \(T(n) = \Theta(f(n))\)
  • «Масса» рекурсии: \(n^{\log_b a} = a^{\log_b n}\) — суммарная работа рекурсивных вызовов по уровням дерева рекурсии
  • Maximum subarray (divide and conquer): \(T(n) = 2T(n/2) + \Theta(n) = \Theta(n \log n)\)
  • Maximum subarray (brute force): \(T(n) = \Theta(n^2)\) при переборе всех \(O(n^2)\) подмассивов
  • Merge sort: \(T(n) = 2T(n/2) + \Theta(n) = \Theta(n \log n)\)
  • Память merge sort: \(\Theta(n)\) дополнительной памяти на слияние
  • Quicksort (лучший/средний баланс): \(T(n) = 2T(n/2) + \Theta(n) = \Theta(n \log n)\)
  • Quicksort (худший случай): \(T(n) = T(n-1) + \Theta(n) = \Theta(n^2)\)
  • Partition: \(\Theta(n)\) на сканирование и перестановки

4. Примеры

4.1. Анализ временной сложности функции обработки связного списка (Набор задач 3, Задание 1)

Найдите худшую временную сложность следующей функции в терминах размера входного связного списка \(n\):

/* L — связный список из n элементов */
function process(L)
  if L.is_empty()
    return 0
  else if L.size() == 1
    return L.head.value
  else
    lists = new array of 3 empty linked lists  // три пустых связных списка
    i = 0
    A = L.head
    while A is not None
      if (i mod 4) > 0
        lists[i mod 4].add(A.value)
      A = A.next
      i = i + 1
    result = process(lists[1])
    result += process(lists[2])
    result += process(lists[3])
    return result

(a) Запишите рекуррентное соотношение для \(T(n)\).

(b) Найдите асимптотику \(T(n)\) методом подстановки, деревом рекурсии или master method. Покажите шаги явно: формальные определения или прямые ссылки на master theorem.

Нажмите, чтобы увидеть решение

Ключевая идея: разобрать структуру функции — сколько элементов попадает в каждую рекурсивную подзадачу, — затем составить и решить рекуррентность.

(a) Рекуррентность

  1. Структура функции:

    • База: пустой список и список из одного элемента обрабатываются за \(\Theta(1)\)
    • Рекурсия: один проход по всем \(n\) элементам (while), распределение по трём подспискам
    • Главный вопрос: сколько элементов в каждом подсписке?
  2. Распределение по подспискам:

    • В lists[i mod 4] добавляют только при `(i mod 4) > 0$
    • Для \(i = 0, 1, 2, \ldots, n-1\):
      • \(i \equiv 0 \pmod{4}\): элемент не добавляют
      • \(i \equiv 1 \pmod{4}\): в lists[1]
      • \(i \equiv 2 \pmod{4}\): в lists[2]
      • \(i \equiv 3 \pmod{4}\): в lists[3]
    • На каждые 4 подряд идущих элемента распределяются ровно 3 (индексы 1, 2, 3 из 0, 1, 2, 3)
    • Всего в три подсписка попадает примерно \(3n/4\) элементов
  3. Точнее:

    • Если \(n = 4k\), распределяется ровно \(3k\) элементов — в среднем по \(k = n/4\) на список
    • Если \(n = 4k + r\) при \(0 < r < 4\), всего порядка \(3n/4\)
    • В худшем случае размеры близки к \(\lceil n/4 \rceil\), \(\lceil n/4 \rceil\), \(\lceil n/4 \rceil\)
  4. Рекуррентность: \[T(n) = \begin{cases} \Theta(1) & \text{if } n \le 1 \\ \Theta(n) + 3T(n/4) & \text{if } n > 1 \end{cases}\]

    где \(\Theta(n)\) — работа цикла while (проход по \(n\) элементам), а \(3T(n/4)\) — три рекурсивных вызова на подсписках размера порядка \(n/4\).

(b) Асимптотика через master theorem

  1. Параметры:

    • \(a = 3\) (три рекурсивных вызова)
    • \(b = 4\) (размер подзадачи порядка \(n/4\))
    • \(f(n) = \Theta(n)\) (нерекурсивная часть — цикл while)
  2. «Масса» рекурсии: \[n^{\log_b a} = n^{\log_4 3} = n^{\log 3 / \log 4} = n^{\approx 0.792}\]

    Точнее, \(\log_4 3 = \frac{\log 3}{\log 4} = \frac{\ln 3}{\ln 4} \approx 0.7924\)

  3. Сравнение \(f(n) = \Theta(n)\) с \(n^{\log_4 3} = n^{\approx 0.792}\):

    • \(f(n) = \Theta(n^1)\)
    • \(n^{\log_4 3} = \Theta(n^{0.792})\)
    • Так как \(1 > 0.792\), имеем \(f(n) \gg n^{\log_4 3}\) — доминирует \(f(n)\)
  4. Case 3 master theorem:

    • Нужно: \(f(n) = \Omega(n^{\log_4 3 + \epsilon})\) при некотором \(\epsilon > 0\)
    • Возьмём \(\epsilon = 0.208\), тогда \(\log_4 3 + \epsilon = 1\)
    • Тогда \(n = \Omega(n^{1}) = \Omega(n)\)
    • Regularity condition: \(af(n/b) \le c \cdot f(n)\) при \(c < 1\)
    • \(3 \cdot (n/4) = 3n/4\)
    • Нужно \(3n/4 \le c n\) — выполняется при \(c = 3/4 < 1\)
  5. Case 3: \[T(n) = \Theta(f(n)) = \Theta(n)\]

Ответ:

(a) \(T(n) = 3T(n/4) + \Theta(n)\)

(b) По master theorem (Case 3): \(T(n) = \Theta(n)\)

Несмотря на рекурсию, функция работает за линейное время: на каждом уровне доминирует локальная работа \(\Theta(n)\), а подзадачи достаточно быстро сжимаются по размеру.

4.2. Решение рекуррентностей методом master theorem (Набор задач 3, Задание 2)

Решите каждую из рекуррентностей с помощью master theorem. Для каждой укажите, применим ли master method; если да — какой case (1, 2 или 3), явно проверьте условия и дайте ответ в \(\Theta\)-нотации.

  1. \(T(n) = 4T(n/16) + \sqrt{n} \cdot \log_2 n\)
  2. \(T(n) = 5T(4n/9) + n^2\)
  3. \(T(n) = 9T(n/4) + \log_2(n!)\)
  4. \(T(n) = 3T(\sqrt[3]{n}) + n\)
  5. \(T(n) = \frac{1}{2026}T(2026n) + n\log_{2026}n\)
Нажмите, чтобы увидеть решение

(1) \(T(n) = 4T(n/16) + \sqrt{n} \cdot \log_2 n\)

  1. Параметры: \(a = 4\), \(b = 16\), \(f(n) = \sqrt{n} \cdot \log_2 n = n^{1/2} \log_2 n\)

  2. «Масса» рекурсии: \[n^{\log_b a} = n^{\log_{16} 4} = n^{\log_{16} 4}\]

    Заметим: \(\log_{16} 4 = \frac{\log 4}{\log 16} = \frac{2\log 2}{4\log 2} = \frac{1}{2}\), значит \(n^{\log_{16} 4} = n^{1/2} = \sqrt{n}\).

  3. Сравнение с \(n^{1/2}\): \(f(n) = n^{1/2} \log_2 n = \Theta(n^{1/2} \log^1 n)\) — это Case 2 при \(k = 1\).

  4. Case 2 (Cormen et al., Theorem 4.1): \(T(n) = \Theta(n^{\log_b a} \log^{k+1} n) = \Theta(\sqrt{n} \log^2 n)\).

Ответ: \(T(n) = \Theta(\sqrt{n} \log^2 n)\) (Case 2)

(2) \(T(n) = 5T(4n/9) + n^2\)

  1. Применимость master theorem: каноническая форма \(T(n) = aT(n/b) + f(n)\) предполагает \(b > 1\) и размер подзадачи именно \(n/b\).
  2. Почему в таком виде «не лезет»: здесь явно \(T(4n/9)\); в учебных формулировках master theorem часто требуют запись с константным делителем \(b\) в знаменателе аргумента \(T(\cdot)\) в стандартном виде; данная запись сразу не совпадает с «шаблоном» из теоремы так же прозрачно, как \(T(n/b)\).
  3. Вывод: master method к этой строке не применяют в рамках стандартной проверки; для точного ответа нужны другие приёмы (подстановка, дерево рекурсии).

Ответ: master method неприменим (в смысле стандартной формы из конспекта).

(3) \(T(n) = 9T(n/4) + \log_2(n!)\)

  1. Упростим \(\log_2(n!)\): по Стирлингу \(\log(n!) = \Theta(n \log n)\), точнее \(\log_2(n!) = \Theta(n \log_2 n)\), то есть \(f(n) = \Theta(n \log n)\).
  2. Параметры: \(a = 9\), \(b = 4\).
  3. «Масса» рекурсии: \(n^{\log_4 9}\), где \(\log_4 9 = \log_2 3 \approx 1.585\).
  4. Сравнение: \(n\log n\) растёт медленнее \(n^{1.585}\); при подходящем \(\epsilon > 0\) выполняется \(f(n) = O(n^{\log_4 9 - \epsilon})\) — это Case 1.
  5. Case 1: \(T(n) = \Theta(n^{\log_4 9}) = \Theta(n^{\log_2 3})\).

Ответ: \(T(n) = \Theta(n^{\log_2 3})\) (Case 1)

(4) \(T(n) = 3T(\sqrt[3]{n}) + n\)

  1. Размер подзадачи \(n^{1/3}\) — не константное \(n/b\) в стандартной форме master theorem.
  2. Замена переменной (идея): \(S(m) = T(3^m)\) даёт \(S(m) = 3S(m/3) + 3^m\); здесь \(f(m) = 3^m\) не полиномиально по \(m\) — стандартный master theorem в домене \(m\) тоже не подходит.
  3. Вывод: к исходной рекуррентности master method в стандартной форме неприменим.

Ответ: неприменим (размер подзадачи не \(n/b\) с константным \(b > 1\) в нужном смысле).

(5) \(T(n) = \frac{1}{2026}T(2026n) + n\log_{2026}n\)

  1. Коэффициент при \(T\) меньше 1, размер подзадачи \(2026n > n\) — это не \(T(n) = aT(n/b) + f(n)\) с \(a \ge 1\), \(b > 1\).
  2. Такая «рекурсия» не соответствует нормальной модели алгоритма: подзадачи растут, база не достигается.
  3. Вывод: master theorem неприменим, запись некорректна как типичная рекуррентность времени работы.

Ответ: неприменим (\(a = \frac{1}{2026} < 1\), подзадачи растут).

4.3. Модифицированный merge sort с insertion sort (Набор задач 3, Задание 3)

Рассмотрите модификацию merge sort: рекурсивное деление останавливается, когда размер подмассива не больше \(k\) (\(k \le n\)). Для подмассива размера \(k\) фаза conquer — сортировка insertion sort.

(a) Запишите псевдокод модифицированного алгоритма.

(b) Какова временная сложность в худшем случае (через \(n\) и \(k\))? Ответ в \(\Theta\)-нотации с обоснованием.

(c) Какова временная сложность в лучшем случае (через \(n\) и \(k\))? Ответ в \(\Theta\)-нотации с обоснованием.

Нажмите, чтобы увидеть решение

(a) Псевдокод

HYBRID-MERGE-SORT(A, p, r, k):
  if r - p + 1 <= k:
    // база: insertion sort на маленьком куске
    INSERTION-SORT(A, p, r)
  else:
    // рекурсия: деление и merge
    q = floor((p + r) / 2)
    HYBRID-MERGE-SORT(A, p, q, k)           // левая половина
    HYBRID-MERGE-SORT(A, q + 1, r, k)      // правая половина
    MERGE(A, p, q, r)                       // слияние

INSERTION-SORT(A, p, r):
  // стандартный insertion sort на A[p..r]
  for i = p + 1 to r:
    key = A[i]
    j = i - 1
    while j >= p and A[j] > key:
      A[j + 1] = A[j]
      j = j - 1
    A[j + 1] = key

(b) Худший случай

  1. Подмассивы длиннее \(k\) делятся и сливаются; на размере \(\le k\) вызывается insertion sort за \(\Theta(k^2)\) в худшем случае.

  2. Глубина деления до «листьев» размера \(\le k\): порядка \(\log_2(n/k)\); число листьев порядка \(n/k\).

  3. Insertion sort: \((n/k)\) вызовов по \(\Theta(k^2)\) суммарно дают \(\Theta(nk)\).

  4. Слияния: на каждом из \(\log_2(n/k)\) уровней суммарно \(\Theta(n)\) работы ⇒ \(\Theta(n\log(n/k))\).

  5. Итого: \[T_{\text{worst}}(n) = \Theta(nk) + \Theta(n\log(n/k)) = \Theta(nk + n\log(n/k))\]

    Так как \(\log(n/k) = \log n - \log k\), выражение согласуется с \(\Theta(nk + n\log n)\) в типичных оговорках по поглощению членов.

    Частные случаи: при \(k = O(1)\) получаем \(\Theta(n\log n)\); при \(k = \Theta(n)\) доминирует \(\Theta(n^2)\).

Ответ (b): \(T_{\text{worst}}(n, k) = \Theta(nk + n\log(n/k))\) (эквивалентно \(\Theta(nk + n(\log n - \log k))\)).

(c) Лучший случай

  1. Insertion sort на отсортированном вводе даёт \(\Theta(s)\) на подмассив размера \(s \le k\) ⇒ на листе \(\Theta(k)\), всего \((n/k)\cdot\Theta(k) = \Theta(n)\).
  2. Слияния по-прежнему \(\Theta(n)\) на уровень, уровней \(\log_2(n/k)\)\(\Theta(n\log(n/k))\).
  3. Итого: \[T_{\text{best}}(n) = \Theta(n) + \Theta(n\log(n/k)) = \Theta(n\log(n/k))\]

Ответ (c): \(T_{\text{best}}(n, k) = \Theta(n\log(n/k))\).

Кратко: худший — \(\Theta(nk + n\log(n/k))\); лучший — \(\Theta(n\log(n/k))\). Малая константа \(k\) (например, 10) даёт практичный гибрид с поведением близким к \(\Theta(n\log n)\).

4.4. Решить \(T(n) = 2T(n/4) + 1\) через master theorem (Лекция 3, Пример 1)

Решите рекуррентность и дайте асимптотическую оценку.

Нажмите, чтобы увидеть решение

Ключевая идея: выписать \(a\), \(b\), \(f(n)\) и применить подходящий case master theorem.

  1. Параметры: \(a = 2\), \(b = 4\), \(f(n) = 1\) (константная работа divide/combine).
  2. «Масса» рекурсии: \(n^{\log_b a} = n^{\log_4 2} = n^{1/2} = \sqrt{n}\).
  3. Сравнение: \(f(n) = 1 = O(n^{1/2 - \epsilon})\) при \(\epsilon = 1/2\)Case 1.
  4. Case 1: \(T(n) = \Theta(n^{\log_4 2}) = \Theta(\sqrt{n})\).

Ответ: \(T(n) = \Theta(\sqrt{n})\)

4.5. Решить \(T(n) = 2T(n/4) + \sqrt{n}\) через master theorem (Лекция 3, Пример 2)
Нажмите, чтобы увидеть решение

Ключевая идея: если \(f(n)\) совпадает с \(n^{\log_b a}\) с точностью до логарифмического множителя — Case 2.

  1. Параметры: \(a = 2\), \(b = 4\), \(f(n) = \sqrt{n} = n^{1/2}\).
  2. «Масса» рекурсии: \(n^{\log_4 2} = n^{1/2}\).
  3. Сравнение: \(f(n) = \Theta(n^{1/2} \log^k n)\) при \(k = 0\).
  4. Case 2: \(T(n) = \Theta(n^{\log_4 2} \log^{0+1} n) = \Theta(\sqrt{n} \log n)\).

Ответ: \(T(n) = \Theta(\sqrt{n} \log n)\)

4.6. Решить \(T(n) = 2T(n/4) + n\) через master theorem (Лекция 3, Пример 3)
Нажмите, чтобы увидеть решение

Ключевая идея: если \(f(n)\) доминирует над \(n^{\log_b a}\) и выполнена regularity conditionCase 3.

  1. Параметры: \(a = 2\), \(b = 4\), \(f(n) = n\).
  2. «Масса» рекурсии: \(n^{\log_4 2} = \sqrt{n}\).
  3. Сравнение: \(f(n) = n\); проверка \(f(n) = \Omega(n^{1/2 + \epsilon})\) при \(\epsilon = 1/2\)
  4. Regularity condition: \(2 \cdot (n/4) \le c n\) при \(c = 1/2 < 1\)
  5. Case 3: \(T(n) = \Theta(f(n)) = \Theta(n)\).

Ответ: \(T(n) = \Theta(n)\)

4.7. Трассировка merge sort (Лекция 3, Пример 4)

Отсортируйте массив \([5, 2, 4, 6, 1, 3]\) с помощью merge sort; проследите шаги divide и merge.

Нажмите, чтобы увидеть решение

Ключевая идея: рекурсивно делить пополам, затем сливать уже отсортированные куски.

  1. Исходный массив: \([5, 2, 4, 6, 1, 3]\)
  2. Первое деление:
    • слева: \([5, 2, 4]\)
    • справа: \([6, 1, 3]\)
  3. Левая половина \([5, 2, 4]\):
    • деление: \([5]\) и \([2, 4]\)
    • дальше \([2, 4]\): \([2]\) и \([4]\)
    • merge: \([2, 4]\), затем \([5]\) и \([2, 4]\)\([2, 4, 5]\)
  4. Правая половина \([6, 1, 3]\):
    • деление: \([6]\) и \([1, 3]\)
    • дальше \([1, 3]\): \([1]\) и \([3]\)
    • merge: \([1, 3]\), затем \([6]\) и \([1, 3]\)\([1, 3, 6]\)
  5. Финальный merge:
    • слева \([2, 4, 5]\), справа \([1, 3, 6]\)
    • сравнения дают \([1, 2, 3, 4, 5, 6]\)

Ответ: \([1, 2, 3, 4, 5, 6]\)

4.8. Трассировка partition в quicksort (Лекция 3, Пример 5)

Выполните partition для массива \([4, 9, 5, 1, 4, 7, 3, 6, 8, 2]\), взяв последний элемент (2) в качестве pivot.

Нажмите, чтобы увидеть решение

Ключевая идея: переставить элементы так, чтобы слева были \(\le\) pivot, справа — \(>\) pivot.

  1. Исходный массив: \([4, 9, 5, 1, 4, 7, 3, 6, 8, 2]\), pivot \(x = 2\) (последний элемент).

  2. Процесс: \(i = -1\); \(j\) от 0 до 8 (pivot на индексе 9 не трогаем).

    Шаг \(j\) \(A[j]\) Сравнение Действие Массив \(i\)
    1 0 4 \(4 > 2\) пропуск \([4, 9, 5, 1, 4, 7, 3, 6, 8, 2]\) -1
    2 1 9 \(9 > 2\) пропуск \([4, 9, 5, 1, 4, 7, 3, 6, 8, 2]\) -1
    3 2 5 \(5 > 2\) пропуск \([4, 9, 5, 1, 4, 7, 3, 6, 8, 2]\) -1
    4 3 1 \(1 \le 2\) swap \(A[0]\) и \(A[3]\) \([1, 9, 5, 4, 4, 7, 3, 6, 8, 2]\) 0
    5 4 4 \(4 > 2\) пропуск \([1, 9, 5, 4, 4, 7, 3, 6, 8, 2]\) 0
    6 5 7 \(7 > 2\) пропуск \([1, 9, 5, 4, 4, 7, 3, 6, 8, 2]\) 0
    7 6 3 \(3 > 2\) пропуск \([1, 9, 5, 4, 4, 7, 3, 6, 8, 2]\) 0
    8 7 6 \(6 > 2\) пропуск \([1, 9, 5, 4, 4, 7, 3, 6, 8, 2]\) 0
    9 8 8 \(8 > 2\) пропуск \([1, 9, 5, 4, 4, 7, 3, 6, 8, 2]\) 0
  3. Постановка pivot на место: swap \(A[i+1]\) и \(A[9]\)\(A[1]\) было 9) → \([1, 2, 5, 4, 4, 7, 3, 6, 8, 9]\).

  4. Итог: слева от pivot элементы \(\le 2\): \([1]\); pivot на индексе 1; справа \([5, 4, 4, 7, 3, 6, 8, 9]\).

Ответ: после partition: \([1, 2, 5, 4, 4, 7, 3, 6, 8, 9]\); pivot на индексе 1.

4.9. Максимальный подмассив методом divide and conquer (Лекция 3, Пример 6)

Найдите максимальный подмассив для \([2, -1, 3, 4, 1, -5]\).

Нажмите, чтобы увидеть решение

Ключевая идея: делим массив, рекурсивно ищем максимум в половинах и отдельно проверяем подмассивы, пересекающие середину.

  1. Массив: \([2, -1, 3, 4, 1, -5]\) (индексы 1–6)
  2. Первое деление при \(m = 3\):
    • слева: \([2, -1, 3]\)
    • справа: \([4, 1, -5]\)
  3. Левая половина \([2, -1, 3]\):
    • деление при \(m = 2\): \([2]\) и \([-1, 3]\)
    • в \([2]\): максимум \([2]\), сумма 2
    • в \([-1, 3]\): максимум среди кусков даёт \([3]\) с суммой 3; пересечение даёт сумму 2
    • Итог слева: \([3]\), сумма 3
  4. Правая половина \([4, 1, -5]\):
    • аналогично получаем максимум справа \([4, 1]\) с суммой 5
  5. Пересечение через исходную середину \(m = 3\):
    • лучший суффикс слева от индекса 3: сумма 3 (элемент 3)
    • лучший префикс справа от индекса 4: \(4 + 1 = 5\)
    • crossing: сумма \(3 + 5 = 8\), подмассив \([3, 4, 1]\)
  6. Сравнение трёх кандидатов: \(3\), \(5\), \(8\)глобальный максимум \([3, 4, 1]\) с суммой 8.

Ответ: максимальный подмассив — \([3, 4, 1]\), сумма 8.

4.10. Время работы merge sort (Лекция 3, Пример 7)

Для рекуррентности merge sort \(T(n) = 2T(n/2) + \Theta(n)\) найдите решение через master theorem.

Нажмите, чтобы увидеть решение

Ключевая идея: классический Case 2 для merge sort.

  1. Параметры: \(a = 2\), \(b = 2\), \(f(n) = \Theta(n)\) (линейное слияние).
  2. «Масса» рекурсии: \(n^{\log_b a} = n\).
  3. Сравнение: \(f(n) = \Theta(n^1 \log^k n)\) при \(k = 0\).
  4. Case 2: \(T(n) = \Theta(n \log n)\).
  5. Смысл: \(\Theta(n)\) работы на каждом из \(\Theta(\log n)\) уровней дерева рекурсии.

Ответ: \(\Theta(n \log n)\).

4.11. Худший случай quicksort (Лекция 3, Пример 8)

Покажите, что quicksort в худшем случае имеет сложность \(\Theta(n^2)\), выписав и решив рекуррентность.

Нажмите, чтобы увидеть решение

Ключевая идея: если pivot всегда крайний, разбиение максимально перекошено.

  1. Рекуррентность: \(T(n) = T(n-1) + T(0) + \Theta(n) = T(n-1) + \Theta(n)\).
  2. Развёртка: \(T(n) \le T(n-1) + cn \le T(n-2) + c(n-1) + cn \le \cdots\)
  3. Сумма: арифметическая прогрессия даёт \(\Theta(n^2)\).
  4. Master theorem: здесь не в стандартной форме \(b > 1\), но развёртка уже даёт \(\Theta(n^2)\).

Ответ: худший случай quicksort\(\Theta(n^2)\).

4.12. Упражнения на master theorem (Лекция 3, Задание 1)

Решите каждую рекуррентность через master theorem; укажите case и обоснуйте.

  1. \(T(n) = 3T(n/2) + n^2\)
  2. \(T(n) = 4T(n/2) + n^2\)
  3. \(T(n) = T(n/2) + 2^n\)
  4. \(T(n) = 16T(n/4) + n\)
Нажмите, чтобы увидеть решение

(1) \(T(n) = 3T(n/2) + n^2\): \(n^{\log_2 3} \approx n^{1.585}\); \(f(n) = n^2\) доминирует — Case 3 с проверкой regularity (\(3/4 < 1\)). Ответ: \(\Theta(n^2)\).

(2) \(T(n) = 4T(n/2) + n^2\): \(n^{\log_2 4} = n^2\)Case 2 при \(k = 0\). Ответ: \(\Theta(n^2 \log n)\).

(3) \(T(n) = T(n/2) + 2^n\): \(n^{\log_2 1} = 1\); доминирует \(2^n\)Case 3 (проверка regularity для экспоненты). Ответ: \(\Theta(2^n)\).

(4) \(T(n) = 16T(n/4) + n\): \(n^{\log_4 16} = n^2\); \(f(n) = n\) меньше — Case 1. Ответ: \(\Theta(n^2)\).